/*
 * $Id: ComponentChangeMonitor.as 29 2009-05-30 13:12:19Z fuller.sethm $
 * 
 * Author: Seth M. Fuller
 * Copyright: 2008
 *
 *  Permission is hereby granted, free of charge, to any person
 *  obtaining a copy of this software and associated documentation
 *  files (the "Software"), to deal in the Software without
 *  restriction, including without limitation the rights to use,
 *  copy, modify, merge, publish, distribute, sublicense, and/or
 *  sell copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following
 *  conditions:
 *
 *  The above copyright notice and this permission notice shall be
 *  included in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 *  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 *  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 *  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 *  OTHER DEALINGS IN THE SOFTWARE.
 *
 *
 * This file is licensed under the BSD (revised) open source
 * license.
 *
 * This is part of the flex-solder components hosted at
 * flex-solder.googlecode.com.
 * 
 */
/**
 * This class monitors the children of a Container for user initiated
 * changes. This class just uses the existing Flex events and only
 * monitors whether or not a component's content has been changed
 * without storing its before and after values. I did this for
 * simplicity.  This class register a CHANGE event, which is fired
 * only when the component's contents are modified through user input.
 *
 * The ComponentChangeMonitor class can also accept a list of
 * components to ignore by setting its ignoreComponents Array to an
 * Array containing UIComponents you do not want to monitor for
 * changes. I added this capibility because on the project I am
 * working on currently, I have a form with a list of session dates
 * displayed in a List control and I didn't want to have the user's
 * selection of a different session be recorded as a change. The only
 * changes that are important on this form are when a user modifies
 * any of the session data for the currently selected session.
 *
 * 
 *
 */

package com.googlecode.flex_solder.monitors {
  import flash.events.Event;
  import flash.events.EventDispatcher;

  import mx.events.FlexEvent;

  import mx.events.ChildExistenceChangedEvent;

  import mx.core.Container;
  import mx.core.UIComponent;

  /**
   * The ComponentChangeMonitor class registers Change event listeners 
   * for each component in the container passed to it.
   *
   * Every UIComponent has some form of CHANGE event, which is fired only
   * when the contents of the UIComponent changes through user input.
   *
   * By default all children of the container are included to be
   * monitored for user changes when initChangeListeners is
   * called. The initChangeListeners method recursively calls itself
   * when it encounters a child that is a container. 
   *
   * If you want to listen to all components in an application you can
   * call initChangeListener on the main Application (Flex) or
   * WindowedApplication (AIR) object of your application.
   *
   * To specify more fine-grained change monitoring call the
   * initChangeMonitor for each container you want to monitor for
   * changes. For instance an employee information add/edit form or an
   * order add/edit form.
   *
   * You don't have to create all of the components for a container
   * before calling initChangeListener, since by default
   * ComponentChangeMonitor attaches a
   * ChildExistenceChangedEvent.CHILD_ADD event to each Container it
   * encounters and then attaches an Event.CHANGE event to the newly
   * added component when it receives the CHILD_ADD event. To turn off
   * listening for new children to a Container set the
   * useAddChildListener Boolean variable to false before calling
   * initChangeListener on the top level Container you want to
   * monitor.
   *
   * Use the ignoreComponents Array to specify components that will be
   * ignored when the user makes changes to them. Components that
   * would probably not be monitored for user changes include:
   * 
   *     A DataGrid of employees that is only used to select the
   *     employee to edit on a detail form would be good candidates
   *     for exclusion from change monitoring.
   *
   *     A List of customer orders used to select an order to view or
   *     edit on a Order Detail form.
   *
   */
  public class ComponentChangeMonitor extends EventDispatcher {

    private var _debugTraceMessages:Boolean = false;

    /**
     * If true, a ChildExistenceChangedEvent.CHILD_ADD event listener
     * will be added to each container, so that when a new child is
     * add to the container it will have an event listener added to it
     * to monitor for changes.
     * 
     */
    private var _useAddChildListener:Boolean = true;

    /**
     * This variable is set to true by the hasChangesListener event
     * listener method when a component is modified by user input.
     *
     * You should call the setSaved() method to reset the hasChanges
     * variable to false.
     */
    private var _hasChanges:Boolean = false;

    /**
     * Assiociative array of UIComponents to listen to for changes.
     */
    private var _changeComponentsList:Object = {};

    /**
     * Array of Container objects whose components are listened to for
     * changes. 
     */
    private var _changeContainers:Array = [];

    /**
     * Array of UIComponents to ignore when listening for changes.
     */
    private var _ignoreComponents:Array = [];

    /**
     * Array of UIComponents to include when listening for
     * changes. This array is used to specify a list of components
     * that will be the only components listened to for changes.
     */
    private var _includeComponents:Array = [];

    public function ComponentChangeMonitor() {

    }

    /** 
     * This function steps through all of the UIComponents for the
     * container passed to it. If the UIComponent is a Container, then
     * initChangeListeners calls itself recursively to initialize
     * listeners for all of the child components of that container.
     * 
     *  @param Container 
     * 
     * @return void
     */
    public function initChangeListeners(container:Container):void {

      attachAddChildEventListener(container);

      for (var idx:int = 0; idx < container.numChildren; idx++) {
	var uiComp:UIComponent = container.getChildAt(idx) as UIComponent;
	if (!isIncluded(uiComp)) {
	  continue;
	}
	if ( uiComp is Container ) {
	  debugTrace('uiComp.name=' + uiComp.name);

	  _changeContainers.push(uiComp);
	  if (Container(uiComp).numChildren > 0) {
	    this.initChangeListeners( uiComp as Container );
	  }

	}
	else {
	    addChangeListener(uiComp);
	}
      }
    }

    private function hasChangesListener( event:Event ):void {
      hasChanges = true;
      debugTrace(' hasChangesListener hasChanges = ' + hasChanges
	    + ' TG:'
	    + (null != event.target ? event.target.name : "NULL")
	    + ' CTG:' + (null != event.currentTarget
			 ? event.currentTarget.name : "NULL"));

    }

    /** 
     * This function checks a UIComponent against the list of components
     * to ignore and returns true if the component should be ignored
     * (i.e. No change listener will be attached to this control).
     * 
     * @param UIComponent 
     * 
     * @return	true - If the component should be ignored for changes.
     *		false - If the component should be monitored for changes.
     */
    private function isIgnored(comp:UIComponent):Boolean {
      var ignoreComp:Boolean = false;
      if (null == _ignoreComponents) {
	return ignoreComp;
      }
      if (null == comp) {
	return true;
      }
      for ( var idx:int = 0; idx < this._ignoreComponents.length; idx++ ) {
	var uiComp:UIComponent = _ignoreComponents[idx] as UIComponent;
	if (null == uiComp) {
	  continue;
	}
	if (comp.uid == uiComp.uid) {
	  debugTrace('ignoring: ' + uiComp.uid);
	  ignoreComp = true;
	  break;
	}
      }
      return ignoreComp;
    }		

    /** 
     * This function checks a UIComponent against the list of
     * components to include and returns true if the component should
     * be included for change listening.  If the includeComponents
     * array is not initialized with any components, then all
     * components not in ignoreComponents (if any) are included for
     * change listening.
     * 
     * @param UIComponent 
     * 
     * @return	true - If the component should be monitored for changes.
     *		false - If the component should be ignored for changes.
     */
    private function isIncluded(comp:UIComponent):Boolean {
      var includeComp:Boolean = false;
      var ignoreComp:Boolean = false;
      if (null == comp) {
	return false;
      }

      ignoreComp = isIgnored(comp);
      if (ignoreComp) {
	return !ignoreComp;
      }
      if (null == _includeComponents || _includeComponents.length == 0) {
	return true;
      }

      for ( var idx:int = 0; idx < this._includeComponents.length; idx++ ) {
	var uiComp:UIComponent = _includeComponents[idx] as UIComponent;
	if (comp.uid == uiComp.uid) {
	  debugTrace('ignoring: ' + uiComp.uid);
	  includeComp = true;
	  break;
	}
      }
      return includeComp;
    }		

    /**
     * Removes the hasChangesListener from each UIComponent not in 
     * ignoreComponents.
     */
    public function removeChangeListeners():void
    {
      for ( var key:String in _changeComponentsList) {
	var uiComp:UIComponent = _changeComponentsList[ key ] as UIComponent;
	uiComp.removeEventListener(Event.CHANGE, hasChangesListener);
      }
    }

    public function traceChangeComponents():void {
      for ( var key:String in _changeComponentsList) {
	var uiComp:UIComponent = _changeComponentsList[ key ] as UIComponent;
// 	trace('traceChangeListeners:' + key);
      }
    }
		
    /**
     * Call removeChangeListeners to remove the change listeners from
     * all components in the _changeComponents array, then removes all
     * components from the _changeComponents array.
     */
     public function resetComponentArray():void {
       removeChangeListeners();
       for (var key:String in _changeComponentsList) {
	 trace('delete key=' + key);
	 delete _changeComponentsList[key];
       }
     }

    private function debugTrace(traceMsg:String):void {
      if (_debugTraceMessages) {
	trace(traceMsg);
      }
    }

    /**
     * Call the setSaved function to set
     * ComponentChangeMonitor.hasChanges to false.
     */
    public function setSaved():void {
      _hasChanges = false;
    }

    /**
     * addChangeListener - This method add the Event.CHANGE event
     * listener to the UIComponent, so that the ComponentChangeMonitor
     * will be updated when a change occurs to the data in the control
     * due to user action.
     */
    public function addChangeListener(uiComp:UIComponent):void {
      if (!_changeComponentsList.hasOwnProperty(uiComp.uid)) {
	debugTrace(uiComp.uid + ':willTrigger(Event.CHANGE)' 
	      + uiComp.willTrigger(Event.CHANGE));

	_changeComponentsList[uiComp.uid] = uiComp;
	uiComp.addEventListener(Event.CHANGE, hasChangesListener);
	debugTrace(uiComp.uid + ' Added listener for: ' + Event.CHANGE
		   + ' Success=' + uiComp.hasEventListener(Event.CHANGE));
      }
      else {
	debugTrace('ALREADY ADDED: addEventListener(Event.CHANGE):'
		   + ' uiComp.name=' + uiComp.name);
      }
    }

    private function addChildListener(event:ChildExistenceChangedEvent):void {
      var uiComp:UIComponent = event.relatedObject as UIComponent;
      var container:Container = event.target as Container;
      if (null != uiComp) {
	debugTrace('addChildListener uiComp.uid=' + uiComp.uid);
	if (uiComp is Container) {
	  initChangeListeners(Container(uiComp));
	}
	else {
	  addChangeListener(uiComp);
	}
      }
      else {
	debugTrace('container.uid='+container.uid + ' NULL uiComp');
      }
    }

    protected function attachAddChildEventListener(container:Container):void {
      if (_useAddChildListener) {
	debugTrace('attachAddChildEventListener:' + container.uid);
	container.addEventListener(ChildExistenceChangedEvent.CHILD_ADD, 
				   addChildListener);
      }
      
    }


    public function get ignoreComponents():Array {
      return _ignoreComponents;
    }

    public function set ignoreComponents(ignoreComponents:Array):void {
      _ignoreComponents = ignoreComponents;
    }

    public function get includeComponents():Array {
      return _includeComponents;
    }

    public function set includeComponents(includeComponents:Array):void {
      _includeComponents = includeComponents;
    }

    [Bindable("valueCommit")]
      public function get hasChanges():Boolean {
      return _hasChanges;
    }

    public function set hasChanges(hasChanges:Boolean):void {
      _hasChanges = hasChanges;
      this.dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT));
    }

    public function get debugTraceMessages():Boolean {
      return _debugTraceMessages;
    }

    public function set debugTraceMessages(debugTraceMessages:Boolean):void {
      _debugTraceMessages = debugTraceMessages;
    }

    public function get useAddChildListener():Boolean {
      return _useAddChildListener;
    }

    public function set useAddChildListener(useAddChildListener:Boolean):void {
      _useAddChildListener = useAddChildListener;
    }

  }
}
